package com.ekeyman.securecreditlib.aop;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;

import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import com.ekeyman.securecreditlib.annotations.Encrypt;
import com.ekeyman.securecreditlib.business.RegistryBO;
import com.ekeyman.securecreditlib.domain.CardTransaction;
import com.ekeyman.securecreditlib.domain.CreditCard;
import com.ekeyman.securecreditlib.domain.Registry;
import com.ekeyman.securecreditlib.service.EkeymanService;
import com.ekeyman.securecreditlib.util.SecureCreditUtils;
import com.ekeymanlib.dto.EncryptionKeys;

@Aspect 
@Component 
public class EncryptionAspect {
	
	private final String ENCRYPT_DATA = "encrypt";
	private final String DECRYPT_DATA = "decrypt";
	
    private Log log = LogFactory.getLog(this.getClass());
    
    @Autowired
    private CacheManager cacheManager;
    
    @Autowired
    private EkeymanService ekeymanService;
    
    @Autowired
    private RegistryBO registryBO;
    
    @Pointcut("execution(* com.ekeyman.securecreditlib.business..save*(..))") 
    public void encryptBeforeSaving() {}

    /*
     * Each entity object is modified before saving. Each field within the object is interrogated for the @Encrypt annotation.
     * The clear text string is retrieved using the appropriate get method. The encryption keys are fetched from ekeyman. The 
     * string is encrypted and replaced in the entity using the appropriate set method.
     */
	@Before("encryptBeforeSaving()")
    @Order(1)
    public void encryptionBefore(JoinPoint joinPoint) throws Throwable {
    	
    	log.debug("encryptBeforeSaving...The method " + joinPoint.getSignature().getName()+ "() begins with " + Arrays.toString(joinPoint.getArgs()));
    	log.debug("encryptBeforeSaving...Target class : "+ joinPoint.getTarget().getClass().getName());
    	
    	Object[] o = joinPoint.getArgs();
    	if(o != null && o.length > 0){
    		processEntity(o[0], ENCRYPT_DATA);
     	}
     }
    
    @Pointcut("execution(* com.ekeyman.securecreditlib.business..find*(..))") 
    public void decryptAfterFinding() {}

    /*
     * Each entity object is modified after being retrieved from the database. Each field within the object is interrogated 
     * for the @Encrypt annotation. The cipher text string is retrieved using the appropriate get method. The encryption keys 
     * are fetched from ekeyman. The string is decrypted and replaced in the entity using the appropriate set method.
     */
    @AfterReturning(pointcut="decryptAfterFinding()", returning = "result")  
    @Order(2)
    public Object decryptionAfter(JoinPoint joinPoint, Object result) throws Throwable {
    	
    	log.debug("decryptAfterFinding...The method " + joinPoint.getSignature().getName()+ "() begins with " + Arrays.toString(joinPoint.getArgs()));
    	log.debug("decryptAfterFinding...Target class : "+ joinPoint.getTarget().getClass().getName());
    	 
    	try {
            
        	if(result != null){
        		processEntity(result, DECRYPT_DATA);
        	}            
        	
        	log.debug("decryptAfterFinding...The method " + joinPoint.getSignature().getName()+ "() ends with " + result);
            return result;
        } 
    	catch (IllegalArgumentException e){
    		log.debug("decryptAfterFinding...Illegal argument "+ Arrays.toString(joinPoint.getArgs()) + " in "+ joinPoint.getSignature().getName() + "()");
            throw e;
        }        
    }

    
    @Pointcut("execution(* com.ekeyman.securecreditlib.business..grid*(..))") 
    public void decryptAfterGridListing() {}

    /*
     * 
     * Each entity object in a list is modified after being retrieved from the database. Each field within the  
     * object is interrogated for the @Encrypt annotation. The cipher text string is retrieved using the appropriate 
     * get method. The encryption keys are fetched from ekeyman. The string is decrypted and replaced in the entity  
     * using the appropriate set method.
     */
    @SuppressWarnings("unchecked")
    @AfterReturning(pointcut="decryptAfterGridListing()", returning = "result")  
    @Order(3)
    public Object decryptionAfterGridListing(JoinPoint joinPoint, Object result) throws Throwable {
    	
    	log.debug("decryptionAfterGridListing...The method " + joinPoint.getSignature().getName()+ "() begins with " + Arrays.toString(joinPoint.getArgs()));
    	log.debug("decryptionAfterGridListing...Target class : "+ joinPoint.getTarget().getClass().getName());
    	 
    	try {
            
        	if(result != null){
        		for(Object entity: (List)result){
        			processEntity(entity, DECRYPT_DATA);
        		}
        	}            
        	
        	log.debug("decryptionAfterGridListing...The method " + joinPoint.getSignature().getName()+ "() ends with " + result);
            return result;
        } 
    	catch (IllegalArgumentException e){
    		log.debug("decryptionAfterGridListing...Illegal argument "+ Arrays.toString(joinPoint.getArgs()) + " in "+ joinPoint.getSignature().getName() + "()");
            throw e;
        }        
    }
    
    private void processEntity(Object entity, String operation) {
		for (Field f : entity.getClass().getDeclaredFields()) {
			if (f.isAnnotationPresent(Encrypt.class) && f.getType().getName().equalsIgnoreCase("java.lang.String")) {
	            try {
	            	log.debug("processEntity...@Encrypt annotation field name: " + f.getName());

	            	String getMethodName = "get" + Character.toUpperCase(f.getName().charAt(0)) + f.getName().substring(1);
		            	String setMethodName = "set" + Character.toUpperCase(f.getName().charAt(0)) + f.getName().substring(1);
		            	log.debug("processEntity...getMethodName: " + getMethodName);
		            	log.debug("processEntity...setMethodName: " + setMethodName);
	            	
	            	Object[] parms = null;
	            	Method gm = f.getDeclaringClass().getMethod(getMethodName);
	            	String unprocessedText = (String)gm.invoke(entity, parms);
	            	log.debug("processEntity...getMethodName value: " + unprocessedText);
	            	
	            	/*
	            	 * Encryption keys are cached to avoid unnecessary webservice 
	            	 * calls to ekeyman. It is possible that an entity can contain
	            	 * multiple string fields annotated with @Encrypt. The cache 
	            	 * duration is only 10 seconds because it is possible that a new
	            	 * private key can be entered into the registry if it is changed
	            	 * by the ekeyman administrator.
	            	 */
	            	EncryptionKeys encryptionKeys = null;
	            	Cache cache = cacheManager.getCache("com.ekeyman");
	    			Element element = cache.get("EncryptionKeys");
	    			if(element != null) {
	    				encryptionKeys = (EncryptionKeys) element.getObjectValue();
	    			}
	    			else {
		            	Registry registry = getRegistryBO().getRegistry();
		            	// Encryption keys should ALWAYS exist during decryption
						encryptionKeys = getEkeymanService().getEncryptionKeys(
								"internal", 
								registry.getPublicKey(), 
								registry.getPrivateKey(),
								"App Server");
						
						Element cacheElement = new Element("EncryptionKeys", encryptionKeys);
						cache.put(cacheElement);
	    			}

	            	String processedTextText = unprocessedText;
	            	
	            	if(operation.equalsIgnoreCase(ENCRYPT_DATA)){
	            		processedTextText = SecureCreditUtils.encrypt(encryptionKeys, unprocessedText);
	            	}
	            	else if(operation.equalsIgnoreCase(DECRYPT_DATA)){
	            		processedTextText = SecureCreditUtils.decrypt(encryptionKeys, unprocessedText);
	            	}
	            	
	            	parms = new Object[1];
	            	Class[] par=new Class[1];
	            	par[0] = String.class;
	            	Method sm = f.getDeclaringClass().getMethod(setMethodName, par);
	            	parms[0] = processedTextText;
	            	sm.invoke(entity, parms);
	            	
	            	if(entity instanceof CreditCard){
	            		log.debug("processEntity...setMethodName value: " + ((CreditCard)entity).getCreditCardNumber());
	            	}
	            	
	            	if(entity instanceof CardTransaction){
	            		log.debug("processEntity...setMethodName value: " + ((CardTransaction)entity).getTransactionNumber());
	            	}            	
		            } 
	            catch (Throwable ex) {
	            	log.debug("processEntity...ex:" + ex.getMessage());
	            }
	         }
		}   
    }
    
	public void setEkeymanService(EkeymanService ekeymanService) {
		this.ekeymanService = ekeymanService;
	}

	public EkeymanService getEkeymanService() {
		return ekeymanService;
	}

	public void setRegistryBO(RegistryBO registryBO) {
		this.registryBO = registryBO;
	}

	public RegistryBO getRegistryBO() {
		return registryBO;
	}
}
